1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This file contains the command line parsing and configuration loading from file. 7 8 The data flow is to first load the configuration from the file then parse the command line. 9 This allows the user to override the configuration via the CLI. 10 */ 11 module code_checker.cli; 12 13 import std.exception : collectException, ifThrown; 14 import std.typecons : Tuple, Flag; 15 import logger = std.experimental.logger; 16 17 import code_checker.types : AbsolutePath, Path; 18 19 @safe: 20 21 enum AppMode { 22 none, 23 help, 24 helpUnknownCommand, 25 normal, 26 initConfig, 27 dumpConfig, 28 } 29 30 /// Configuration options only relevant for static code checkers. 31 struct ConfigStaticCode { 32 import code_checker.engine.types : Severity; 33 34 /// Filter results from analyzers on this severity. 35 Severity severity; 36 } 37 38 /// Configuration options only relevant for clang-tidy. 39 struct ConfigClangTidy { 40 /// Checks to toggle on/off 41 string[] checks; 42 43 /// Arguments to be baked into the checks parameter 44 string[] options; 45 46 /// Argument to the filter parameter 47 string headerFilter; 48 49 /// Apply fix hints. 50 bool applyFixit; 51 52 /// Apply fix hints even though they result in errors. 53 bool applyFixitErrors; 54 55 /// The clang-tidy binary to use. 56 string binary = "clang-tidy"; 57 } 58 59 /// Configuration data for the compile_commands.json 60 struct ConfigCompileDb { 61 import code_checker.compile_db : CompileCommandFilter; 62 63 /// Command to generate the compile_commands.json 64 string generateDb; 65 66 /// Raw user input via either config or cli 67 string[] rawDbs; 68 69 /// Either a path to a compilation database or a directory to search for one in. 70 AbsolutePath[] dbs; 71 72 /// Do not remove the merged compile_commands.json 73 bool keep; 74 75 /// Flags the user wants to be automatically removed from the compile_commands.json. 76 CompileCommandFilter flagFilter; 77 } 78 79 /// Settings for the compiler 80 struct Compiler { 81 /// Additional flags the user wants to add besides those that are in the compile_commands.json. 82 string[] extraFlags; 83 } 84 85 /// Settings for logging. 86 struct Logging { 87 import code_checker.logger : VerboseMode; 88 89 VerboseMode verbose; 90 91 /// If logging to files should be done. 92 bool toFile; 93 94 /// Directory to log to. 95 AbsolutePath dir; 96 } 97 98 /// Configuration of how to use the program. 99 struct Config { 100 AppMode mode; 101 102 ConfigStaticCode staticCode; 103 ConfigClangTidy clangTidy; 104 ConfigCompileDb compileDb; 105 Compiler compiler; 106 MiniConfig miniConf; 107 Logging logg; 108 109 /// If set then only analyze these files. 110 string[] analyzeFiles; 111 112 /// Returns: a config object with default values. 113 static Config make() @safe { 114 import code_checker.compile_db : defaultCompilerFlagFilter, 115 CompileCommandFilter; 116 117 Config c; 118 setClangTidyFromDefault(c); 119 c.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 1); 120 return c; 121 } 122 123 string toTOML(Flag!"fullConfig" full) @trusted { 124 import std.algorithm : joiner; 125 import std.ascii : newline; 126 import std.array : appender, array; 127 import std.format : format; 128 import std.utf : toUTF8; 129 import std.traits : EnumMembers; 130 import code_checker.engine : Severity; 131 132 auto app = appender!(string[])(); 133 app.put("[defaults]"); 134 app.put(format("# only report issues with a severity >= to this value (%(%s, %))", 135 [EnumMembers!Severity])); 136 app.put(format(`severity = "%s"`, staticCode.severity)); 137 app.put(null); 138 139 app.put("[compiler]"); 140 app.put("# extra flags to pass on to the compiler"); 141 app.put(`# extra_flags = [ "-std=c++11", "-Wextra", "-Wdocumentation" ]`); 142 app.put(null); 143 144 app.put("[compile_commands]"); 145 app.put("# command to execute to generate compile_commands.json"); 146 app.put(format(`generate_cmd = "%s"`, compileDb.generateDb)); 147 app.put("# search for compile_commands.json in this paths"); 148 if (compileDb.dbs.length == 0 || compileDb.dbs.length == 1 149 && compileDb.dbs[0] == Path("./compile_commands.json").AbsolutePath) 150 app.put(format("search_paths = %s", ["./compile_commands.json"])); 151 else 152 app.put(format("search_paths = %s", compileDb.dbs)); 153 if (full) { 154 app.put("# flags to remove when analyzing a file in the DB"); 155 app.put(format("# filter = [%(%s,\n%)]", compileDb.flagFilter.filter)); 156 app.put("# compiler arguments to skip from the beginning. Needed when the first argument is NOT a compiler but rather a wrapper"); 157 app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs)); 158 } 159 app.put(null); 160 161 app.put("[clang_tidy]"); 162 app.put("# clang-tidy binary to use"); 163 app.put(format(`# binary = "%s"`, clangTidy.binary)); 164 app.put("# arguments to -header-filter"); 165 app.put(format(`header_filter = "%s"`, clangTidy.headerFilter)); 166 if (full) { 167 app.put("# checks to use"); 168 app.put(format("checks = [%(%s,\n%)]", clangTidy.checks)); 169 app.put("# options affecting the checks"); 170 app.put(format("options = [%(%s,\n%)]", clangTidy.options)); 171 } 172 app.put(null); 173 174 return app.data.joiner(newline).toUTF8; 175 } 176 } 177 178 /// Minimal config to setup path to config file and workdir. 179 struct MiniConfig { 180 /// Value from the user via CLI, unmodified. 181 string rawWorkDir; 182 183 /// Converted to an absolute path. 184 AbsolutePath workDir; 185 186 /// Value from the user via CLI, unmodified. 187 string rawConfFile = ".code_checker.toml"; 188 189 /// The configuration file that has been loaded 190 AbsolutePath confFile; 191 } 192 193 /// Returns: minimal config to load settings and setup working directory. 194 MiniConfig parseConfigCLI(string[] args) @trusted nothrow { 195 import std.path : dirName; 196 static import std.getopt; 197 198 MiniConfig conf; 199 200 try { 201 std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough, 202 "workdir", "none not visible to the user", &conf.rawWorkDir, 203 "c|config", "none not visible to the user", &conf.rawConfFile); 204 conf.confFile = Path(conf.rawConfFile).AbsolutePath; 205 if (conf.rawWorkDir.length == 0) { 206 conf.rawWorkDir = conf.confFile.dirName; 207 } 208 conf.workDir = Path(conf.rawWorkDir).AbsolutePath; 209 } catch (Exception e) { 210 logger.error("Invalid cli values: ", e.msg).collectException; 211 logger.trace(conf).collectException; 212 } 213 214 return conf; 215 } 216 217 void parseCLI(string[] args, ref Config conf) @trusted { 218 import std.algorithm : map, among, filter; 219 import std.array : array; 220 import std.format : format; 221 import std.path : dirName, buildPath; 222 import std.traits : EnumMembers; 223 import code_checker.engine.types : Severity; 224 import code_checker.logger : VerboseMode; 225 static import std.getopt; 226 227 bool verbose_info; 228 bool verbose_trace; 229 std.getopt.GetoptResult help_info; 230 try { 231 string config_file = ".code_checker.toml"; 232 string[] compile_dbs; 233 string[] src_filter; 234 string workdir; 235 string logdir = "."; 236 bool dump_conf; 237 bool init_conf; 238 239 // dfmt off 240 help_info = std.getopt.getopt(args, 241 "clang-tidy-bin", "clang-tidy binary to use", &conf.clangTidy.binary, 242 "clang-tidy-fix", "apply suggested clang-tidy fixes", &conf.clangTidy.applyFixit, 243 "clang-tidy-fix-errors", "apply suggested clang-tidy fixes even if they result in compilation errors", &conf.clangTidy.applyFixitErrors, 244 "compile-db", "path to a compilationi database or where to search for one", &compile_dbs, 245 "c|config", "load configuration (default: .code_checker.toml)", &config_file, 246 "dump-config", "dump the full, detailed configuration used", &dump_conf, 247 "f|file", "if set then analyze only these files (default: all)", &conf.analyzeFiles, 248 "init", "create an initial config to use", &init_conf, 249 "keep-db", "do not remove the merged compile_commands.json when done", &conf.compileDb.keep, 250 "log", "create a logfile for each analyzed file", &conf.logg.toFile, 251 "logdir", "path to create logfiles in (default: .)", &logdir, 252 "severity", format("report issues with a severity >= to this value (default: style) %s", [EnumMembers!Severity]), &conf.staticCode.severity, 253 "vverbose", "verbose mode is set to trace", &verbose_trace, 254 "v|verbose", "verbose mode is set to information", &verbose_info, 255 "workdir", "use this path as the working directory when programs used by analyzers are executed (default: where .code_checker.toml is)", &workdir, 256 ); 257 // dfmt on 258 conf.mode = AppMode.normal; 259 if (help_info.helpWanted) 260 conf.mode = AppMode.help; 261 else if (init_conf) 262 conf.mode = AppMode.initConfig; 263 else if (dump_conf) 264 conf.mode = AppMode.dumpConfig; 265 conf.logg.verbose = () { 266 if (verbose_trace) 267 return VerboseMode.trace; 268 if (verbose_info) 269 return VerboseMode.info; 270 return VerboseMode.minimal; 271 }(); 272 273 // use a sane default which is to look in the current directory 274 if (compile_dbs.length == 0 && conf.compileDb.dbs.length == 0) { 275 compile_dbs = ["./compile_commands.json"]; 276 } else if (compile_dbs.length != 0) { 277 conf.compileDb.rawDbs = compile_dbs; 278 } 279 280 if (conf.logg.toFile) 281 conf.logg.dir = Path(logdir).AbsolutePath; 282 283 // dfmt off 284 conf.compileDb.dbs = conf 285 .compileDb.rawDbs 286 .filter!(a => a.length != 0) 287 .map!(a => Path(buildPath(conf.miniConf.workDir, a)).AbsolutePath) 288 .array; 289 // dfmt on 290 } catch (std.getopt.GetOptException e) { 291 // unknown option 292 logger.error(e.msg); 293 conf.mode = AppMode.helpUnknownCommand; 294 } catch (Exception e) { 295 logger.error(e.msg); 296 conf.mode = AppMode.helpUnknownCommand; 297 } 298 299 void printHelp() @trusted { 300 import std.getopt : defaultGetoptPrinter; 301 import std.format : format; 302 import std.path : baseName; 303 304 defaultGetoptPrinter(format("usage: %s\n", args[0].baseName), help_info.options); 305 } 306 307 if (conf.mode.among(AppMode.help, AppMode.helpUnknownCommand)) { 308 printHelp; 309 return; 310 } 311 } 312 313 /** Load the configuration from file. 314 * 315 * Example of a TOML configuration 316 * --- 317 * [defaults] 318 * check_name_standard = true 319 * --- 320 */ 321 void loadConfig(ref Config rval) @trusted { 322 import std.algorithm; 323 import std.array : array; 324 import std.file : exists, readText; 325 import std.path : dirName, buildPath; 326 import toml; 327 328 if (!exists(rval.miniConf.confFile)) 329 return; 330 331 static auto tryLoading(string configFile) { 332 auto txt = readText(configFile); 333 auto doc = parseTOML(txt); 334 return doc; 335 } 336 337 TOMLDocument doc; 338 try { 339 doc = tryLoading(rval.miniConf.confFile); 340 } catch (Exception e) { 341 logger.warning("Unable to read the configuration from ", rval.miniConf.confFile); 342 logger.warning(e.msg); 343 return; 344 } 345 346 alias Fn = void delegate(ref Config c, ref TOMLValue v); 347 Fn[string] callbacks; 348 349 void defaults__check_name_standard(ref Config c, ref TOMLValue v) { 350 import std.traits : EnumMembers; 351 import code_checker.engine.types : toSeverity, Severity; 352 353 auto s = toSeverity(v.str); 354 if (s.isNull) { 355 logger.warningf("Unknown severity level %s. Using default: style", v.str); 356 logger.warningf("valid values are: %s", [EnumMembers!Severity]); 357 c.staticCode.severity = Severity.style; 358 } else { 359 c.staticCode.severity = s; 360 } 361 } 362 363 callbacks["defaults.severity"] = &defaults__check_name_standard; 364 365 callbacks["compile_commands.search_paths"] = (ref Config c, ref TOMLValue v) { 366 c.compileDb.rawDbs = v.array.map!(a => a.str).array; 367 }; 368 callbacks["compile_commands.generate_cmd"] = (ref Config c, ref TOMLValue v) { 369 c.compileDb.generateDb = v.str; 370 }; 371 callbacks["compile_commands.filter"] = (ref Config c, ref TOMLValue v) { 372 import code_checker.compile_db : FilterClangFlag; 373 374 c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array; 375 }; 376 callbacks["compile_commands.skip_compiler_args"] = (ref Config c, ref TOMLValue v) { 377 c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer; 378 }; 379 callbacks["clang_tidy.header_filter"] = (ref Config c, ref TOMLValue v) { 380 c.clangTidy.headerFilter = v.str; 381 }; 382 callbacks["clang_tidy.checks"] = (ref Config c, ref TOMLValue v) { 383 c.clangTidy.checks = v.array.map!(a => a.str).array; 384 }; 385 callbacks["clang_tidy.options"] = (ref Config c, ref TOMLValue v) { 386 c.clangTidy.options = v.array.map!(a => a.str).array; 387 }; 388 callbacks["compiler.extra_flags"] = (ref Config c, ref TOMLValue v) { 389 c.compiler.extraFlags = v.array.map!(a => a.str).array; 390 }; 391 392 void iterSection(ref Config c, string sectionName) { 393 if (auto section = sectionName in doc) { 394 // specific configuration from section members 395 foreach (k, v; *section) { 396 if (auto cb = sectionName ~ "." ~ k in callbacks) 397 (*cb)(c, v); 398 else 399 logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName); 400 } 401 } 402 } 403 404 iterSection(rval, "defaults"); 405 iterSection(rval, "clang_tidy"); 406 iterSection(rval, "compile_commands"); 407 iterSection(rval, "compiler"); 408 } 409 410 /// Returns: default configuration as embedded in the binary 411 void setClangTidyFromDefault(ref Config c) @safe nothrow { 412 import std.algorithm; 413 import std.array; 414 import std.ascii : newline; 415 416 static auto readConf(immutable string raw) { 417 // dfmt off 418 return raw 419 .splitter(newline) 420 // remove empty lines 421 .filter!(a => a.length != 0) 422 // remove comments 423 .filter!(a => !a.startsWith("#")) 424 .array; 425 // dfmt on 426 } 427 428 immutable raw_checks = import("clang_tidy_checks.conf"); 429 immutable raw_options = import("clang_tidy_options.conf"); 430 431 c.clangTidy.checks = readConf(raw_checks); 432 c.clangTidy.options = readConf(raw_options); 433 c.clangTidy.headerFilter = ".*"; 434 }